/*
 * client.cpp
 *
 *  Created on: 2021年8月16日
 *      Author: pengzc
 */

#include <http/http_client.h>
#include <string.h>
#include <algorithm>
#include <signal.h>
#include <unistd.h>
#include <fstream>
#include "utils/Log.h"
#include "manager/ConfigManager.h"
#include <base/log.hpp>
#include <base/strings.hpp>
#include <base/files.hpp>
#include "slist.h"
#include "easy.h"
#include <base/scope_guard.hpp>

namespace http {

HTTPClient::HTTPClient() {
  curlInit();
  curl_ = curl_easy_init();
  if (curl_ == NULL) {
    LOGE("curl_easy_init failed");
  }

  timeout_ = 0;
  connection_timeout_ = 0;

  ssl_verify_ = true;
  aborted_ = false;

  low_speed_limit_.bytes = 0;
  low_speed_limit_.seconds = 0;

  debug_ = false;

  ca_info_file_path_ = CONFIGMANAGER->getResFilePath("cacert.pem");

}

HTTPClient::~HTTPClient() {
  if (curl_) {
    curl_easy_cleanup(curl_);
  }
}

HTTPResponse HTTPClient::perform(const HTTPRequest& request) {
  return perform(request, "", NULL);
}

HTTPResponse HTTPClient::perform(const HTTPRequest& request,
    ProgressCallback progress) {
  return perform(request, "", progress);
}

HTTPResponse HTTPClient::perform(const HTTPRequest& request,
    const std::string& save_path, ProgressCallback progress) {

  Mutex::Autolock lock(do_mutex_);

  if (curl_ == NULL) {
    throwHTTPException(CURLE_FAILED_INIT, "invalid curl object");
  }
  curl_easy_reset(curl_);

  aborted_ = false;
  HTTPResponse response;
  std::string url = request.url_;
  std::string header_string;
  CURLcode res = CURLE_OK;

  SCOPE_EXIT {
    if (response.dst_file_ != NULL) {
      fflush(response.dst_file_);
      fclose(response.dst_file_);
      response.dst_file_ = NULL;
    }
  };

  /** set query URL */
  curl_easy_setopt(curl_, CURLOPT_URL, url.c_str());

#if 0
  if (request.method_.compare("download") != 0) {
      /** set callback function */
      curl_easy_setopt(this->curl_, CURLOPT_WRITEFUNCTION,
                       Helpers::write_callback);
      /** set data object to pass to callback function */
      curl_easy_setopt(this->curl_, CURLOPT_WRITEDATA, &ret);
  } else
#endif

  /** set data callback */
  curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION,
      curlResponseWriteCallback);
  /** set data object to pass to callback function */
  curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response);

  if (request.resume_from_ != 0) {
    curl_easy_setopt(curl_, CURLOPT_RESUME_FROM, (long)request.resume_from_);
  }

  if (!request.body_.empty()) {
    /** set post fields */
    curl_easy_setopt(this->curl_, CURLOPT_POSTFIELDS,
        request.body_.c_str());
    curl_easy_setopt(this->curl_, CURLOPT_POSTFIELDSIZE,
        request.body_.size());
  }

  if (request.method_.compare("GET") == 0) {

  } else if (request.method_.compare("POST") == 0) {
    /** Now specify we want to POST data */
    curl_easy_setopt(this->curl_, CURLOPT_POST, 1L);

    if (request.mime_ != NULL) {
//      struct curl_httppost *formpost = NULL;
//      struct curl_httppost *lastptr  = NULL;
//      for (auto it = request.multiparts_.begin(); it != request.multiparts_.end(); ++it) {
//        curl_formadd(&formpost, &lastptr,
//            CURLFORM_PTRNAME, it->first.c_str(),
//            CURLFORM_PTRCONTENTS, it->second.data(),
//            CURLFORM_CONTENTSLENGTH, it->second.length(),
//            CURLFORM_END);
//      }

      /** Now specify we want to POST data */
      curl_easy_setopt(this->curl_, CURLOPT_MIMEPOST, request.mime_->mime());
//      curl_easy_setopt(this->curl_, CURLOPT_HTTPPOST, formpost);
    }
  } else {
    curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, request.method_.c_str());
  }

#if 1
  /** set the header callback function */
  curl_easy_setopt(this->curl_, CURLOPT_HEADERFUNCTION, header_callback);
  /** callback object for headers */
  curl_easy_setopt(this->curl_, CURLOPT_HEADERDATA, &response);
#endif

  /** set http headers */
  slist header_list;
  for (HTTPRequest::HeaderFields::const_iterator it = request.header.begin();
      it != request.header.end(); ++it) {
    header_string = it->first;
    header_string += ": ";
    header_string += it->second;
    header_list.add(header_string);
  }
  if (!header_list.empty()) {
    curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, header_list.list());
  }

  progress_callback_ = progress;
  curl_easy_setopt(curl_, CURLOPT_XFERINFOFUNCTION,
      HTTPClient::curlProgressCallback);
  curl_easy_setopt(curl_, CURLOPT_XFERINFODATA, this);
  curl_easy_setopt(curl_, CURLOPT_NOPROGRESS, 0);

  if ((low_speed_limit_.bytes > 0) && (low_speed_limit_.seconds > 0)) {
    curl_easy_setopt(curl_, CURLOPT_LOW_SPEED_LIMIT, low_speed_limit_.bytes);
    curl_easy_setopt(curl_, CURLOPT_LOW_SPEED_TIME, low_speed_limit_.seconds);
  }

  if (!auth_.username.empty()) {
    std::string authString = std::string(auth_.username + ":" + auth_.password);
    curl_easy_setopt(this->curl_, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
    curl_easy_setopt(this->curl_, CURLOPT_USERNAME, auth_.username.c_str());
    curl_easy_setopt(this->curl_, CURLOPT_PASSWORD, auth_.password.c_str());
  }

#if 0
  /** set user agent */
  curl_easy_setopt(this->curl_, CURLOPT_USERAGENT,
                   this->GetUserAgent().c_str());
#endif

  // set timeout
  if (connection_timeout_ > 0) {
    curl_easy_setopt(this->curl_, CURLOPT_CONNECTTIMEOUT_MS,
        connection_timeout_);
  }
  if (timeout_ > 0) {
    curl_easy_setopt(this->curl_, CURLOPT_TIMEOUT_MS, timeout_);
    // dont want to get a sig alarm on timeout
  }

  //允许重定向
  curl_easy_setopt(this->curl_, CURLOPT_FOLLOWLOCATION, 1L);
  curl_easy_setopt(this->curl_, CURLOPT_MAXREDIRS, static_cast<int64_t>(9));

  //多线程下使用超时禁用SIGNAL
  curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1L);

  //指定DNS解析规范为IPV4
  curl_easy_setopt(curl_, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

    //指定DNS
  curl_easy_setopt(curl_, CURLOPT_DNS_SERVERS,
      "114.114.114.114,1.2.4.8,210.2.4.8,119.29.29.29");

  // if provided, supply CA path
  if (base::startswith(request.url_, "https://")) {
    std::string ca = ca_info_file_path_;
    if (ca.empty()) {
      ca = CONFIGMANAGER->getResFilePath("cacert.pem");
    }
    if (!base::exists(ca)) {
      LOGE_TRACE("cacert not found: %s", ca.c_str());
    }
    curl_easy_setopt(curl_, CURLOPT_CAINFO, ca.c_str());
  }

  if (!ssl_verify_) {
    curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYHOST, 0L);
  }

  if (debug_) {
    curl_easy_setopt(curl_, CURLOPT_DEBUGFUNCTION, debugTrace);
    /* the DEBUGFUNCTION has no effect until we enable VERBOSE */
    curl_easy_setopt(curl_, CURLOPT_VERBOSE, 1L);
  }

  if (!save_path.empty()) {
    response.dst_file_ = fopen(save_path.c_str(),
                               request.resume_from_ != 0 ? "ab" : "wb");
    if (response.dst_file_ == NULL) {
      throwHTTPException(CURLE_BAD_FUNCTION_ARGUMENT, "open '%s' error",
                         save_path.c_str());
    }
  }

#if 0
  // set cert file path
  if (!this->certPath.empty()) {
    curl_easy_setopt(this->curl_, CURLOPT_SSLCERT,
                     this->certPath.c_str());
  }

  // set cert type
  if (!this->certType.empty()) {
    curl_easy_setopt(this->curl_, CURLOPT_SSLCERTTYPE,
                     this->certType.c_str());
  }
  // set key file path
  if (!this->keyPath.empty()) {
    curl_easy_setopt(this->curl_, CURLOPT_SSLKEY,
                     this->keyPath.c_str());
  }
  // set key password
  if (!this->keyPassword.empty()) {
    curl_easy_setopt(this->curl_, CURLOPT_KEYPASSWD,
                     this->keyPassword.c_str());
  }

  // set web proxy address
  if (!this->uriProxy.empty()) {
    curl_easy_setopt(this->curl_, CURLOPT_PROXY,
                     uriProxy.c_str());
    curl_easy_setopt(this->curl_, CURLOPT_HTTPPROXYTUNNEL,
                     1L);
  }
#endif

  char error_buffer[CURL_ERROR_SIZE] = { 0 };
  curl_easy_setopt(curl_, CURLOPT_ERRORBUFFER, error_buffer);

  res = curl_easy_perform(curl_);
  if (res != CURLE_OK) {
    throwHTTPException(res, "%s", error_buffer);
  }
  int64_t http_code = 0;
  curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code);
  response.status_code_ = static_cast<int>(http_code);

  curl_easy_getinfo(curl_, CURLINFO_TOTAL_TIME,
                    &response.total_time);
  curl_easy_getinfo(this->curl_, CURLINFO_NAMELOOKUP_TIME,
                    &response.name_lookup_time);
  curl_easy_getinfo(this->curl_, CURLINFO_CONNECT_TIME,
                    &response.connect_time);
  curl_easy_getinfo(this->curl_, CURLINFO_APPCONNECT_TIME,
                    &response.app_connect_time);
  curl_easy_getinfo(this->curl_, CURLINFO_PRETRANSFER_TIME,
                    &response.pre_transfer_time);
  curl_easy_getinfo(this->curl_, CURLINFO_STARTTRANSFER_TIME,
                    &response.start_transfer_time);
  curl_easy_getinfo(this->curl_, CURLINFO_REDIRECT_TIME,
                    &response.redirect_time);
  curl_easy_getinfo(this->curl_, CURLINFO_REDIRECT_COUNT,
                    &response.redirect_count);

  return response;
}

/**
 * @brief set custom Certificate Authority (CA) path
 *
 * @param caInfoFilePath - The path to a file holding the certificates used to
 * verify the peer with. See CURLOPT_CAINFO
 *
 */
void
HTTPClient::setCAInfoFilePath(const std::string& caInfoFilePath) {
  this->ca_info_file_path_ = caInfoFilePath;
}

#if 0
/**
 * @brief set username and password for basic auth
 *
 * @param username
 * @param password
 *
 */
void
HTTPClient::SetBasicAuth(const std::string& username,
                                     const std::string& password) {
  this->basicAuth.username = username;
  this->basicAuth.password = password;
}

#endif

/**
 * @brief set certificate path
 *
 * @param path to certificate file
 *
 */
/**
 * @brief set certificate type
 *
 * @param certificate type (e.g. "PEM" or "DER")
 *
 */
//void
//HttpClient::SetCertType(const std::string& certType) {
//  this->certType = certType;
//}

/**
 * @brief set key path
 *
 * @param path to key file
 *
 */
//void
//HttpClient::SetKeyPath(const std::string& keyPath) {
//  this->keyPath = keyPath;
//}

/**
 * @brief set key password
 *
 * @param key password
 *
 */
//void
//HttpClient::SetKeyPassword(const std::string& keyPassword) {
//  this->keyPassword = keyPassword;
//}

/**
 * @brief set HTTP proxy address and port
 *
 * @param proxy address with port number
 *
 */
//void
//HttpClient::SetProxy(const std::string& uriProxy) {
//  std::string uriProxyUpper = uriProxy;
//  // check if the provided address is prefixed with "http"
//  std::transform(uriProxyUpper.begin(), uriProxyUpper.end(),
//    uriProxyUpper.begin(), ::toupper);
//
//  if ((uriProxy.length() > 0) && (uriProxyUpper.compare(0, 4, "HTTP") != 0)) {
//    this->uriProxy = "http://" + uriProxy;
//  } else {
//    this->uriProxy = uriProxy;
//  }
//}

/**
 * @brief helper function to get called from the actual request methods to
 * prepare the curlHandle for transfer with generic options, perform the
 * request and record some stats from the last request and then reset the
 * handle with curl_easy_reset to its default state. This will keep things
 * like connections and session ID intact but makes sure you can change
 * parameters on the object for another request.
 *
 * @param uri URI to query
 * @param ret Reference to the Response struct that should be filled
 *
 * @return 0 on success and 1 on error
 */
//Response
//HttpClient::performCurlRequest(const std::string& uri, const std::string method_type) {}

/**
 * @brief HTTP GET method
 *
 * @param url to query
 *
 * @return response struct
 */
//Response
//HttpClient::get(const std::string& url) {
//  return this->performCurlRequest(url);
//}
/**
 * @brief HTTP POST method
 *
 * @param url to query
 * @param data HTTP POST body
 *
 * @return response struct
 */
//Response
//HttpClient::post(const std::string& url,
//                             const std::string& data) {
//  /** Now specify we want to POST data */
//  curl_easy_setopt(this->curlHandle, CURLOPT_POST, 1L);
//  /** set post fields */
//  curl_easy_setopt(this->curlHandle, CURLOPT_POSTFIELDS, data.c_str());
//  curl_easy_setopt(this->curlHandle, CURLOPT_POSTFIELDSIZE, data.size());
//
//  return this->performCurlRequest(url);
//}

//Response
//HttpClient::post(const std::string& url,
//    const std::map<std::string, std::string>& form_data) {
//
//  struct curl_httppost *formpost = 0;
//  struct curl_httppost *lastptr  = 0;
//  for (auto it = form_data.begin(); it != form_data.end(); ++it) {
//    curl_formadd(&formpost, &lastptr,
//        CURLFORM_PTRNAME, it->first.c_str(),
//        CURLFORM_PTRCONTENTS, it->second.data(),
//        CURLFORM_CONTENTSLENGTH, it->second.length(),
//        CURLFORM_END);
//  }
//
//  /** Now specify we want to POST data */
//  curl_easy_setopt(this->curlHandle, CURLOPT_HTTPPOST, formpost);
//
//  Response response = this->performCurlRequest(url);
//  curl_formfree(formpost);
//  return response;
//}

void HTTPClient::setConnectTimeout(int ms) {
  connection_timeout_ = ms;
}

void HTTPClient::setTimeout(int ms) {
  timeout_ = ms;
}

void HTTPClient::setLowSpeedLimit(int bytes, int seconds) {
  low_speed_limit_.bytes = bytes;
  low_speed_limit_.seconds = seconds;
}

// trim from start
static inline std::string &ltrim(std::string &s) {  // NOLINT
  s.erase(s.begin(), std::find_if(s.begin(), s.end(),
        std::not1(std::ptr_fun<int, int>(std::isspace))));
  return s;
}

// trim from end
static inline std::string &rtrim(std::string &s) { // NOLINT
  s.erase(std::find_if(s.rbegin(), s.rend(),
        std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
  return s;
}

// trim from both ends
static inline std::string &trim(std::string &s) {  // NOLINT
  return ltrim(rtrim(s));
}

size_t HTTPClient::curlResponseWriteCallback(void* data, size_t size,
    size_t nmemb, void* userdata) {
//  LOGD_TRACE("write %d bytes", size * nmemb);
  HTTPResponse* response = (HTTPResponse*) userdata;
  if (response->dst_file_ == NULL) {
    response->body_.append(reinterpret_cast<char*>(data), size * nmemb);
    return size * nmemb;
  }
  return fwrite(reinterpret_cast<char*>(data), 1, size * nmemb,
      response->dst_file_);
}

static std::string parseReasonText(const std::string& line) {
  auto pos = line.find(" ");
  if (pos == line.npos) {
    return "";
  }
  pos = line.find(" ", pos + 1);
  if (pos == line.npos) {
    return "";
  }
  if (pos + 1 >= line.size()) {
    return "";
  }
  auto t = line.substr(pos + 1);
  trim(t);
  return t;
}

size_t HTTPClient::header_callback(void *data, size_t size,
                                            size_t nmemb, void *userdata) {
  HTTPResponse* r = (HTTPResponse*)userdata;
  std::string header(reinterpret_cast<char*>(data), size*nmemb);

//  LOGD("Header Callback  [%s]", trim(header).c_str());

  if (header.find("HTTP/") == 0) {
    r->reason_text_ = parseReasonText(header);
    trim(r->reason_text_);
  }

  size_t seperator = header.find_first_of(':');
  if ( std::string::npos == seperator ) {
    // roll with non seperated headers...
    trim(header);
    if (0 == header.length()) {
      return (size * nmemb);  // blank line;
    }
    r->header_[header] = "present";
  } else {
    std::string key = header.substr(0, seperator);
    trim(key);
    std::string value = header.substr(seperator + 1);
    trim(value);

    auto it = r->header_.find(key);
    if (it == r->header_.end()) {
      //LOGD("It's new Head Field %s", key.c_str());
      r->header_[key] = value;
    } else {
      //LOGD("Duplicate Head Field %s", key.c_str());
    }
    //LOGD("headers size %d", r->header_.size());
//    LOGD("HHHHHHHHHHHHHead%p %s %s", r,  key.c_str(), value.c_str());
  }
  return (size * nmemb);
}

void HTTPClient::setSSLVerify(bool verify) {
  ssl_verify_ = verify;
}

void HTTPClient::terminate() {
  aborted_ = true;
  Mutex::Autolock lock(do_mutex_);
}

int HTTPClient::curlProgressCallback(void* data, curl_off_t dltotal,
    curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
  HTTPClient* client = (HTTPClient*)data;

  if (client->aborted_) {
    return -1;
  }
  if (client->progress_callback_ != NULL) {
    return client->progress_callback_(dltotal, dlnow, ultotal, ulnow) ? 0 : -1;
  }
  return 0;
}

static void dump(const char *text, unsigned char *ptr, size_t size) {
  LOGD("%s, %10.10ld bytes (0x%8.8lx)\n",
          text, (long)size, (long)size);
}

int HTTPClient::debugTrace(CURL *handle, curl_infotype type, char *data, ::size_t size,
    void *userp) {
  const char *text;
   (void)handle; /* prevent compiler warning */
   (void)userp;

   switch (type) {
   case CURLINFO_TEXT:
     LOGD("== Info: %s", data); // @suppress("No break at end of case")
   default: /* in case a new one is introduced to shock us */
     return 0;

   case CURLINFO_HEADER_OUT:
     text = "=> Send header";
     break;
   case CURLINFO_DATA_OUT:
     text = "=> Send data";
     break;
   case CURLINFO_SSL_DATA_OUT:
     text = "=> Send SSL data";
     break;
   case CURLINFO_HEADER_IN:
     text = "<= Recv header";
     break;
   case CURLINFO_DATA_IN:
     text = "<= Recv data";
     break;
   case CURLINFO_SSL_DATA_IN:
     text = "<= Recv SSL data";
     break;
   }

   dump(text, (unsigned char *)data, size);
   return 0;
}

void HTTPClient::setDebug(bool debug) {
  debug_ = debug;
}

void HTTPClient::setBasicAuth(const std::string& username,
    const std::string& password) {
  auth_.username = username;
  auth_.password = password;
}

} /* namespace http */

